O conjunto de dados empregado nas análises a seguir foi elaborado na etapa de coleta e tratamento deste trabalho (vide: coleta). Em resumo, são dados provenientes de publicações do Twitter referentes à pandemia de COVID-19. Especificamente, o conjunto a seguir contém informações sobre a data de criação, fonte das informações geolocalizadas do dado e cidade e estado de publicações com origem no Brasil entre os dias 19 de fevereiro e 20 de março de 2020.
Vamos iniciar nossa análise carregando o conjunto. Bibliotecas necessárias para a análise serão importadas ao longo do caderno.
import pandas as pd
df = pd.read_csv('tweets_covid_brasil.csv')
df
| created_at | geo_source | estado | cidade | |
|---|---|---|---|---|
| 0 | 2020-02-19 16:42:08+00:00 | user_location | São Paulo | São Paulo |
| 1 | 2020-02-19 19:49:00+00:00 | user_location | São Paulo | São Paulo |
| 2 | 2020-02-19 18:04:27+00:00 | user_location | Minas Gerais | Belo Horizonte |
| 3 | 2020-02-19 20:10:17+00:00 | user_location | São Paulo | NaN |
| 4 | 2020-02-19 18:47:44+00:00 | user_location | Maranhão | NaN |
| ... | ... | ... | ... | ... |
| 1863367 | 2020-03-20 08:44:49+00:00 | user_location | Rondônia | NaN |
| 1863368 | 2020-03-20 10:57:02+00:00 | user_location | NaN | NaN |
| 1863369 | 2020-03-20 12:03:33+00:00 | user_location | NaN | NaN |
| 1863370 | 2020-03-20 09:50:36+00:00 | user_location | NaN | NaN |
| 1863371 | 2020-03-20 10:57:04+00:00 | user_location | NaN | NaN |
1863372 rows × 4 columns
O conjunto possui 1.863.372 registros (ou linhas) e 4 colunas.
Vamos visualizar uma descrição básica dos dados:
df.describe()
| created_at | geo_source | estado | cidade | |
|---|---|---|---|---|
| count | 1863372 | 1863372 | 968815 | 622949 |
| unique | 1083140 | 3 | 28 | 1893 |
| top | 2020-03-15 02:21:53+00:00 | user_location | São Paulo | Rio de Janeiro |
| freq | 15 | 1833373 | 303951 | 106331 |
Através dessa simples descrição já podemos extrair algumas informações interessantes como:
created_at e geo_source possuem uma contagem de linhas iguais ao tamanho do DataFrame, estado e cidade têm menos registros, o que indica a presença de valores nulos.geo_source (o que condiz com o tratamento feito anteriormente), 28 valores únicos para estado e 1893 valores únicos para cidade. Sabemos que no Brasil existem 26 estados, mais o Distrito Federal, então o valor 28 deve ser investigado. Já para as cidades, o valor fica abaixo do número de 5570 municípios que consta na listagem do IBGE.user_location foi a fonte mais comum das informações geolocalizadas desses dados. São Paulo lidera como o estado com maior número de publicações geolocalizadas, mas é a cidade do Rio de Janeiro que detém o maior número de publicações.count e top) porque a biblioteca pandas entendeu que todos os dados são categoricos, o que é verdade para a maioria deles, mas não todos. Vamos abordar a questão dos tipos a seguir.
geo_source, estado e cidade.
df['geo_source'].unique()
array(['user_location', 'place', 'coordinates'], dtype=object)
O campo geo_source contém os valores 'user_location', 'place' e 'coordinates', o que está de acordo com as informações selecionadas no momento da coleta dos dados.
df['estado'].unique()
array(['São Paulo', 'Minas Gerais', 'Maranhão', nan, 'Rio de Janeiro',
'Rio Grande do Sul', 'Santa Catarina', 'Pará', 'Bahia', 'Paraíba',
'Paraná', 'Acre', 'Amazonas', 'Pernambuco', 'Mato Grosso', 'Amapá',
'Espírito Santo', 'Piauí', 'Ceará', 'Rondônia', 'Goiás',
'Rio Grande do Norte', 'Mato Grosso do Sul', 'Tocantins',
'Sergipe', 'Roraima', 'Alagoas', 'Federal District',
'South Region'], dtype=object)
Aqui podemos ver que o campo estado contém os 26 estados brasileiros, o Distrito Federal (que aqui recebeu o nome de 'Federal District'), valores nulos (representado pelo valor 'nan') e um valor chamado 'South Region'. Consultando a documentação dos dados do Twitter, verificamos que o campo state fornecido pode estar relacionado também com província por conta de outros países e, nesse caso, 'South Region' faz referência à toda região sul do Brasil. Mais adiante, podemos analisar a relação dos registros com esse valor para o estado e tentar correlacionar a cidade com o estado.
df['cidade'].unique()
array(['São Paulo', 'Belo Horizonte', nan, ..., 'Ipuaçu', 'Nioaque',
'Conselheiro Mairinck'], dtype=object)
Para o campo cidade, como vimos na descrição acima, a quantidade de valores é muito grande para exibir todos unicamente.
Vamos analisar os tipos de dados de cada uma das colunas:
df.dtypes
created_at object geo_source object estado object cidade object dtype: object
Nesse ponto, todas as colunas são do tipo object o que, pela documentação da biblioteca pandas, representa um tipo de "objeto arbitrário" que "deve ser evitado na medida do possível para desempenho e interoperabilidade com outras bibliotecas e métodos". Cadeias de caracteres também são entendidas pela biblioteca como sendo desse tipo.
Sendo assim, é adequado que os devidos ajustes nos tipos sejam feitos. No caso do conjunto de dados que estamos analisando, o ajuste a ser feito é na coluna referente à data de criação da publicação, uma vez que se trata de um timestamp. As demais colunas podem ser mantidas como o tipo object por se tratarem de cadeia de caracteres.
Para converter o campo created_at, podemos empregar a função to_datetime da própria biblioteca:
df['created_at'] = pd.to_datetime(df['created_at'])
df.dtypes
created_at datetime64[ns, UTC] geo_source object estado object cidade object dtype: object
Agora temos a coluna com o tipo mais adequado para o atributo created_at.
Uma questão importante antes de começarmos a trabalhar com os dados é verificar a quantidade de valores nulos, pois como vimos na seção de descrição dos dados, as colunas estados e cidade têm menos registros que o número total de registros do conjunto de dados. Podemos utilizar o comando a seguir para obter essa contagem:
df.isna().sum()
created_at 0 geo_source 0 estado 894557 cidade 1240423 dtype: int64
O atributo estado tem 894.557 registro nulos, o que também significa dizer que são 968.815 registros que foram preenchidos. Já cidade tem 1.240.423 registros nulos, ou seja, 622.949 registros preenchidos. Isso se deve ao fato de que essas informaçõe não são obrigatoriamente preenchidas ou retornadas pelo Twitter. Vale lembrar que para a etapa de coleta dos dados, bastava que a publicação tivesse origem no Brasil.
Como não é possível inferir o estado e cidade de origem desses dados para preencher os valores ausentes, vamos criar uma categoria chamada 'N.I' (Não Informado) para esses dados. Podemos usar o código abaixo para realizar esse preenchimento:
df.fillna('N.I.', axis=1, inplace=True)
Podemos realizar a contagem dos valores nulos novamente para verificar se a alteração surtiu efeito:
df.isna().sum()
created_at 0 geo_source 0 estado 0 cidade 0 dtype: int64
Para facilitar a manipulação dos dados nas análises a seguir, vamos realizar pequenas tranformações no dado. Primeiramente, vamos criar um campo sigla baseado no nome do estado.
estados = {
'Acre':'AC',
'Alagoas':'AL',
'Amapá':'AP',
'Amazonas':'AM',
'Bahia':'BA',
'Ceará':'CE',
'Federal District':'DF',
'Espírito Santo':'ES',
'Goiás':'GO',
'Maranhão':'MA',
'Mato Grosso':'MT',
'Mato Grosso do Sul':'MS',
'Minas Gerais':'MG',
'Pará':'PA',
'Paraíba':'PB',
'Paraná':'PR',
'Pernambuco':'PE',
'Piauí':'PI',
'Rio de Janeiro':'RJ',
'Rio Grande do Norte':'RN',
'Rio Grande do Sul':'RS',
'Rondônia':'RO',
'Roraima':'RR',
'Santa Catarina':'SC',
'São Paulo':'SP',
'Sergipe':'SE',
'Tocantins':'TO',
'N.I.':'N.I'
}
df['sigla']=df['estado'].map(estados)
df
| created_at | geo_source | estado | cidade | sigla | |
|---|---|---|---|---|---|
| 0 | 2020-02-19 16:42:08+00:00 | user_location | São Paulo | São Paulo | SP |
| 1 | 2020-02-19 19:49:00+00:00 | user_location | São Paulo | São Paulo | SP |
| 2 | 2020-02-19 18:04:27+00:00 | user_location | Minas Gerais | Belo Horizonte | MG |
| 3 | 2020-02-19 20:10:17+00:00 | user_location | São Paulo | N.I. | SP |
| 4 | 2020-02-19 18:47:44+00:00 | user_location | Maranhão | N.I. | MA |
| ... | ... | ... | ... | ... | ... |
| 1863367 | 2020-03-20 08:44:49+00:00 | user_location | Rondônia | N.I. | RO |
| 1863368 | 2020-03-20 10:57:02+00:00 | user_location | N.I. | N.I. | N.I |
| 1863369 | 2020-03-20 12:03:33+00:00 | user_location | N.I. | N.I. | N.I |
| 1863370 | 2020-03-20 09:50:36+00:00 | user_location | N.I. | N.I. | N.I |
| 1863371 | 2020-03-20 10:57:04+00:00 | user_location | N.I. | N.I. | N.I |
1863372 rows × 5 columns
Vamos também separar a coluna created_at em duas colunas: 'data' e 'horario'.
df['data'] = pd.to_datetime(df['created_at']).dt.date
df['horario'] = pd.to_datetime(df['created_at']).dt.time
df
| created_at | geo_source | estado | cidade | sigla | data | horario | |
|---|---|---|---|---|---|---|---|
| 0 | 2020-02-19 16:42:08+00:00 | user_location | São Paulo | São Paulo | SP | 2020-02-19 | 16:42:08 |
| 1 | 2020-02-19 19:49:00+00:00 | user_location | São Paulo | São Paulo | SP | 2020-02-19 | 19:49:00 |
| 2 | 2020-02-19 18:04:27+00:00 | user_location | Minas Gerais | Belo Horizonte | MG | 2020-02-19 | 18:04:27 |
| 3 | 2020-02-19 20:10:17+00:00 | user_location | São Paulo | N.I. | SP | 2020-02-19 | 20:10:17 |
| 4 | 2020-02-19 18:47:44+00:00 | user_location | Maranhão | N.I. | MA | 2020-02-19 | 18:47:44 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 1863367 | 2020-03-20 08:44:49+00:00 | user_location | Rondônia | N.I. | RO | 2020-03-20 | 08:44:49 |
| 1863368 | 2020-03-20 10:57:02+00:00 | user_location | N.I. | N.I. | N.I | 2020-03-20 | 10:57:02 |
| 1863369 | 2020-03-20 12:03:33+00:00 | user_location | N.I. | N.I. | N.I | 2020-03-20 | 12:03:33 |
| 1863370 | 2020-03-20 09:50:36+00:00 | user_location | N.I. | N.I. | N.I | 2020-03-20 | 09:50:36 |
| 1863371 | 2020-03-20 10:57:04+00:00 | user_location | N.I. | N.I. | N.I | 2020-03-20 | 10:57:04 |
1863372 rows × 7 columns
Anteriormente, percebemos a presença do valor 'South Region' no campo estado. Vamos verificar se nos registros onde esse valor ocorre, existem valores para o campo cidade que permitam criar uma função que associe um estado da região sul ao registro. Podemos fazer isso criando uma máscara que verifica se valor da coluna estado é igual a 'South Region' e se o valor da coluna cidade é diferente de "Não Informado":
mascara_south_region = (df['estado'] == 'South Region') & (df['cidade'] != 'N.I.')
df.loc[mascara_south_region]
| created_at | geo_source | estado | cidade | sigla | data | horario |
|---|
Como não existe nenhum registro retornado, não é possível determinar quais os estados.
Após as transformações realizadas, podemos inciar a exploração dos dados. Podemos verificar algumas medidas estatísticas como a média diária de publicações, o menor e o maior número de publicações em um dia:
contagens_data = df.groupby(df['data']).count().rename(columns={"created_at": "publicações"})['publicações']
print("A média de publicações diária foi de %.2f" % contagens_data.mean())
print("O menor nº de publicações foi de %s" % contagens_data.min())
print("O maior nº de publicações foi de %s" % contagens_data.max())
A média de publicações diária foi de 60108.77 O menor nº de publicações foi de 4002 O maior nº de publicações foi de 173003
Podemos observar a distribuição das publicações ao longo das datas, juntamente com as linhas de máximo, mínimo e média:
import matplotlib.pyplot as plt
contagens_data.plot(figsize=(15,5), kind="bar", xlabel='', ylabel='')
plt.axhline(contagens_data.mean(), color='b', linestyle='--', label="Média")
plt.axhline(contagens_data.min(), color='g', linestyle='--', label="Min")
plt.axhline(contagens_data.max(), color='r', linestyle='--', label="Max")
plt.title("Distribuição das publicações [19/02/2020 - 20/03/2020]")
plt.xlabel("Data da publicação")
plt.ylabel("Nº de publicações")
plt.legend()
<matplotlib.legend.Legend at 0x19d05bc2c40>
Através desse gráfico conseguimos notar algumas datas na distribuição nas quais o número de publicações cresce consideravelmente. Vamos considerar algumas dessas datas que estão relacionadas a momentos específicos da pandemia do COVID: 26 de fevereiro, 11, 12 e 13 de março e 17 de março de 2020.
Como também temos o horário das publicações, podemos visualizar a distribuição das publicações por hora do dia. Para isso, utilizaremos um mapa de calor com as datas e horários. Primeiramente vamos instalar uma biblioteca que irá auxiliar na construção do gráfico:
!pip install seaborn
Requirement already satisfied: seaborn in c:\users\flavi\anaconda3\lib\site-packages (0.11.2) Requirement already satisfied: pandas>=0.23 in c:\users\flavi\anaconda3\lib\site-packages (from seaborn) (1.4.3) Requirement already satisfied: scipy>=1.0 in c:\users\flavi\anaconda3\lib\site-packages (from seaborn) (1.9.1) Requirement already satisfied: numpy>=1.15 in c:\users\flavi\anaconda3\lib\site-packages (from seaborn) (1.22.3) Requirement already satisfied: matplotlib>=2.2 in c:\users\flavi\anaconda3\lib\site-packages (from seaborn) (3.5.3) Requirement already satisfied: pillow>=6.2.0 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (9.2.0) Requirement already satisfied: pyparsing>=2.2.1 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (3.0.4) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (1.4.4) Requirement already satisfied: fonttools>=4.22.0 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (4.36.0) Requirement already satisfied: packaging>=20.0 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (21.3) Requirement already satisfied: python-dateutil>=2.7 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (2.8.2) Requirement already satisfied: cycler>=0.10 in c:\users\flavi\anaconda3\lib\site-packages (from matplotlib>=2.2->seaborn) (0.11.0) Requirement already satisfied: pytz>=2020.1 in c:\users\flavi\anaconda3\lib\site-packages (from pandas>=0.23->seaborn) (2022.1) Requirement already satisfied: six>=1.5 in c:\users\flavi\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib>=2.2->seaborn) (1.16.0)
Vamos agrupar as publicações por mês, dia e horário:
import seaborn as sns
s = (df.groupby([pd.to_datetime(df['data']).dt.month,
df['data'],
pd.to_datetime(df['horario'], format='%H:%M:%S').dt.strftime('%H:00')])
['created_at'].count()
.rename_axis(index=['mes','dia','hora'])
)
E agora podemos exibir as informações considerando os meses de fevereiro e março:
meses = {2:'Fevereiro', 3: u'Março'}
fig, axes = plt.subplots(2,1, figsize=(15,10))
for m, ax in zip(s.index.unique('mes'), axes.ravel()):
sns.heatmap(s.loc[m].unstack(level='dia'), ax=ax, cmap="YlGnBu", linewidths=.5)
ax.set_title(f' {meses[m]}')
Através do mapa de calor, é possível notar que, geralmente as publicações são postadas em maior volume a partir das 16h e esse volume vai até às 2h. Os dias com maior número de publicações coincidem com a distribuição
Podemos analisar a distribuição do número de publicações por estado:
contagem_estados = df.loc[df['estado'] != 'N.I.'].groupby(df['sigla']).count()['created_at'].sort_values(ascending=False)
contagem_estados.plot(figsize=(15,5), kind="bar", xlabel='Estado', ylabel='Nº de publicações')
<AxesSubplot:xlabel='Estado', ylabel='Nº de publicações'>
E também visualizar essa contagem de publicações por estado no mapa:
#instalando a plotly
!pip install plotly
Requirement already satisfied: plotly in c:\users\flavi\anaconda3\lib\site-packages (5.10.0) Requirement already satisfied: tenacity>=6.2.0 in c:\users\flavi\anaconda3\lib\site-packages (from plotly) (8.0.1)
# importando as bibliotecas
import plotly as plty #plotly
import plotly.express as px #plotly express
import json #biblioteca para manipular JSON
df_estados_publicacoes = pd.DataFrame(df.loc[df['estado'] != 'N.I.'].groupby(['sigla']).count()[['created_at']].reset_index())
df_estados_publicacoes.rename(columns = {'created_at':'publicações'}, inplace = True)
geojson = json.load(open('brazil_geo.json'))
plty.offline.init_notebook_mode()
fig = px.choropleth_mapbox(
df_estados_publicacoes, #soybean database
locations = "sigla", #define the limits on the map/geography
geojson = geojson, #shape information
color = "publicações", #defining the color of the scale through the database
hover_name = "sigla", #the information in the box
hover_data = ["publicações"],
title = "Publicações por Estado", #title of the map
mapbox_style = "carto-positron", #defining a new map style
center={"lat":-14, "lon": -55},#define the limits that will be plotted
zoom = 3, #map view size
opacity = 0.5, #opacity of the map color, to appear the background,
color_continuous_scale="GnBu",
range_color=(0, 303952),
)
fig.update_geos(fitbounds = "locations", visible = False)
fig.update_layout(height=800)
fig.show()